Simulation sequences

The Simantics Sequences framework (Simantics/Sequences, exposed in Apros as Apros/Sequences) allows discrete-event control logic to be defined in SCL and executed during a running simulation. Sequences share simulation time and data with the solver but do not block the solver thread.

Design properties

Sequences have four key properties:

  • Lightweight — thousands of parallel sequences can run simultaneously without significant overhead.
  • Deterministic — parallel sequences initiated from the same root sequence execute in a deterministic order (unless explicit random number generation is used).
  • Non-exclusive — sequences do not reserve the simulation thread; they interleave with the solver.
  • Restricted — sequences interact with simulator state (variable values) only; model configuration (database operations) cannot be performed from within a sequence.

Sequence monad

data Sequence a (Simantics/Sequences)

Sequence a is a plan of some operations that may happen during the simulation and may take some (simulation) time. A sequence is initiated at a specific time and it may either finish at a specific time or operate forever. If it completes, it retuns a value of type a.

Sequence :: ((a -> <Action,Proc> ()) -> <Action,Proc> ()) -> Sequence a (Simantics/Sequences)

We call the sequence instantaneous, if its duration is zero, i.e, the sequence finishes immediately after starting.

A cooking recipe is an example of a sequence in the real world. Its return value could be for example the success indication of the cooking process.

instance Monad Sequence

In order to build complex sequences from simple primitives, the sequences implement Monad operations and its laws. These are:

return :: Monad a => b -> a b (Prelude)

Inject a value into the monadic type.

(>>=) :: Monad a => a b -> (b -> a c) -> a c (Prelude)

Sequentially compose two actions, passing any value produced by the first as an argument to the second.

The sequence return v has zero duration, does not modify the simulator state, and returns v. The sequence seqA >>= f first behaves like seqA; when it completes and returns a value resultA, it continues as f resultA. In other words, >>= concatenates two sequences and the second sequence may depend on the return value of the first.

No execution occurs when a sequence value is constructed. Execution begins only when runSequence or registerSequence is called.

(>>) :: Monad a => a b -> a c -> a c (Prelude)

Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages."

fmap :: Functor a => (b -> c) -> a b -> a c (Prelude)

Lifts a pure function to the given functor.

join :: Monad a => a (a b) -> a b (Prelude)

The join function is the conventional monad join operator. It removes one level of monadic structure.

For lists, join concatenates a list of lists:

join [[1,2], [3,4]] = [1, 2, 3, 4]
sequence :: FunctorM a => Monad b => a (b c) -> b (a c) (Prelude)

Evaluate each action in the sequence from left to right, and collect the results.

repeatForever :: Monad a => a b -> a c (Prelude)

Sequences the given monadic value infinitely:

repeatForever m = m >> m >> m >> ...

These operations are derived from the primitive monad operations. The sequence seqA >> seqB behaves first like seqA and when it has finished it continues like seqB. The sequence fmap f seq maps the result of the sequence seq by the function f. The sequence join seq first behaves like the sequence seq and then like the sequence seq returned. The sequence sequence seqs executes every sequence in the container seqs sequentially. The container can be for example list or Maybe. The sequence repeatForever seq repeats the sequence seq forever, never returning.

replicateM :: Monad a => Integer -> a b -> a [b] (Prelude)

replicateM n seq runs a sequence n times and collects all results into a list.

Actions

effect Action

<Action> a is an instantaneous operation happening in the simulator and returning a value of type a. It can be a pure reading operation, but may also modify the simulator state. The duration of an action is always zero.

Variable references use Apros syntax: "MODULE_NAME#ATTRIBUTE_NAME".

time :: <Action> Double (Simantics/Sequences)

Gives the current simulation time.

getVar :: Serializable a => String -> <Action> a (Simantics/Sequences)

Returns the current value of a variable

setVar :: Serializable a => String -> a -> <Action> () (Simantics/Sequences)

Sets the value of a variable

execute :: <Action,Proc> a -> Sequence a (Simantics/Sequences)

The sequence execute action is an instantious sequence that executes the operation action in the simulator.

Multiple actions happening at the same time may be written either as separate sequences:

mdo execute (setVar "SP1#SP_VALUE" 13)
    execute (setVar "SP2#SP_VALUE" 14)

or as one sequence with a more complex action:

execute do
    setVar "SP1#SP_VALUE" 13
    setVar "SP2#SP_VALUE" 14

Controlling time

waitStep :: Sequence () (Simantics/Sequences)

The sequence waitStep waits that the simulator takes one simulation step. It is a primitive mechanism that can be used to implement other events by inspecting the simulator state after each time step.

waitUntil :: Double -> Sequence () (Simantics/Sequences)

The sequence waitUntil time waits until the simulation time is at least the given time.

wait :: Double -> Sequence () (Simantics/Sequences)

The sequence wait duration waits that duration seconds elapses from the current simulation time.

waitCondition :: <Action,Proc> Boolean -> Sequence () (Simantics/Sequences)

The sequence waitCondition condition waits until the condition is satisfied.

Parallel execution

fork :: Sequence a -> Sequence () (Simantics/Sequences)

The sequence fork seq is an instantious sequence that creates a new sequence thread behaving like the sequence seq.

halt :: Sequence a (Simantics/Sequences)

The sequence halt ends the current sequence thread and the sequence .

stop :: Sequence a (Simantics/Sequences)

The sequence stop stops all sequence threads, stopping the simulation completely.

fork seq starts seq in parallel with the current sequence. The forked sequence has priority at the same time step: its actions execute before those of the forking sequence at the same simulation instant.

halt stops the current sequence only. Other parallel sequences continue.

stop stops all sequences initiated from the same root sequence. This is the mechanism for signalling a terminal error condition from any thread.

Running sequences

runSequence seq — registers the sequence and starts the simulation if it is not already running. Blocks until all threads have halted or stop is called. Returns Maybe a where a is the return value of the root sequence.

import "Apros/Sequences"
runSequence mdo
    fork $ repeatForever mdo
        waitCondition (getVar "TA01#TA11_LIQ_LEVEL" >= 3.0)
        execute (setVar "BP01#PU11_SPEED_SET_POINT" 0.0)
        wait 1
    fork $ repeatForever mdo
        waitCondition (getVar "TA01#TA11_LIQ_LEVEL" <= 2.0)
        execute (setVar "BP01#PU11_SPEED_SET_POINT" 100.0)
        wait 1

registerSequence seq — registers the sequence in the current experiment without controlling simulation start or stop. Returns an ActionContext that can be used to remove the sequence later. Use this when the simulation is already running and you want to add a new control thread dynamically.

stopActionContext ctx — removes a previously registered sequence from the experiment without stopping the simulation. Use the ActionContext returned by registerSequence.

Semantics

Although simulation sequences support threading, the semantics is deterministic. This is ensured by the following equivalences:

halt >> seqA                                       = halt
stop >> seqA                                       = stop
fork (execute actionA >> seqA) >> seqB             = execute actionA >> fork seqA >> seqB
fork (waitStep >> seqA) >> execute actionB >> seqB = execute actionB >> fork seqA >> seqB
fork (waitStep >> seqA) >> waitStep >> seqB        = waitStep >> fork seqA >> seqB
fork halt >> seqB                                  = seqB
fork seqA >> halt                                  = seqA
fork stop >> seqB                                  = stop
fork (waitStep >> seqA) >> stop                    = stop

Examples

Check that pressure of a point stays below a certain value:

fork mdo
    waitCondition (getVar "POINT1#PO11_PRESSURE" > 120.0)
    execute (print "Error! Error!")
    stop

Check that the valve is closed 10 seconds after the operator presses a button:

fork $ repeatForever mdo
    waitCondition (getVar "BUTTON#BINARY_VALUE")
    fork mdo
        wait 10
        valvePos <- execute (getVar "VALVE#VA11_POSITION")
        if valvePos == 0
        then return () // OK
        else mdo
            execute (print "Error! Error!")
            stop

Custom combinators

Sequences are first-class values, so reusable control patterns can be factored into functions. For example:

forkAndJoin runs two sequences in parallel and waits for both to complete, returning both results as a tuple:

forkAndJoin :: Sequence a -> Sequence b -> Sequence (a, b)
forkAndJoin seqA seqB = mdo
    // Implementation uses fork and shared references
    ...

printElapsedTime wraps any sequence to print its elapsed simulation time on completion:

printElapsedTime :: Sequence a -> Sequence a
printElapsedTime seq = mdo
    t0  <- execute time
    res <- seq
    t1  <- execute time
    execute (print "Elapsed: \(t1 - t0) s")
    return res

Sequences vs SCL scripts

Scripts Sequences
Execution model Sequential, single thread Parallel lightweight threads
Simulation control Can start/stop/configure Interacts with running state only
DB access Full <ReadGraph>/<WriteGraph> Not available
Use case Model building, batch setup Runtime control logic, discrete events